<?php
namespace app\api\controller;

use WeChatPay\Formatter;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Crypto\AesGcm;

/**
 * 微信支付接口
 * Class Wx
 * @package app\api\controller
 */
class PayWX extends MY
{
    const CONFIG = [
        //微信公众号|小程序id
        "appId"      => "wx45e8be6e67961f5c",
        //微信公众号|小程序密钥
        "appSecret"  => "96ad45fdec5e048acb47a9df6179b2b9",
        //微信支付的商户id
        "merchantId" => "1635119425",
        //微信支付的商户api证书序列号
        "merchantApiCertSerialNo" => "7215FD08D87320460DD280B5FADF23A6B4228F80",
        //在商户平台上设置的APIv3密钥
        "apiV3Key"  => "A9pcf8eujf280YpfG4e7bYek7RUgYnPe",
        //微信支付回调地址
        "notifyUrl" => "https://www.xxxxxxxxxxxxxxxxx.com/api.php/pay/wxNotify", //todo
        //请求头设置
        "header"    => [
            "Accept: application/json",
            "Content-Type: application/json; charset=utf-8",
        ],
        //证书目录
        "payCert"   => "public/static/files/wxPayCert/"
    ];

    /**
     * 支付签名
     * 小程序、公众号、app调起支付时需要
     * @param string $api
     * @param string $desc
     * @param string $no
     * @param string $attach
     * @param int $amount
     * @param string $openid
     * @return array
     */
    public function paySign(string $api, string $desc, string $no, string $attach, int $amount, string $openid): array
    {
        $prepay_id = $this->orderCreate($api. $desc, $no, $attach, $amount, $openid);

        $data = [
            "appId"     => self::CONFIG["appId"],
            "timeStamp" => (string)Formatter::timestamp(),
            "nonceStr"  => Formatter::nonce(),
            "prepay_id" => $prepay_id,
        ];
        $signStr = Formatter::joinedByLineFeed(...array_values($data));
        $paySign = $this->signature($signStr);

        $data += [
            "signType"  => "RSA",
            "paySign"   => $paySign
        ];
        return $data;
    }

    /**
     * 向微信服务器下单
     * 商户系统先调用该接口在微信支付服务后台生成预支付交易单，返回正确的预支付交易会话标识后再按Native、JSAPI、APP等不同场景生成交易串调起支付。
     * @param string $api
     * @param string $desc
     * @param string $no
     * @param string $attach
     * @param int $total
     * @param string $openid
     * @return mixed
     */
    public function orderCreate(string $api, string $desc, string $no, string $attach, int $total, string $openid="")
    {
        $data = [
            "appid"         => self::CONFIG["appId"],      //由微信生成的应用ID，全局唯一
            "mchid"         => self::CONFIG["merchantId"], //直连商户的商户号，由微信支付生成并下发。
            "description"   => $desc,                      //商品描述
            "out_trade_no"  => $no,                        //商户系统内部订单号，只能是数字、大小写字母_-*且在同一个商户号下唯一
            "attach"        => $attach,                    //附加数据
            "notify_url"    => self::CONFIG["notifyUrl"],  //异步接收微信支付结果通知的回调地址
            "time_expire"   => date("c", time()+300), //订单失效时间，遵循rfc3339标准格式，设置5分钟失效，也可以不设置
            "amount" => [
                "total"    => $total,  //订单总金额，单位为分。
                "currency" => "CNY",   //货币类型
            ],
        ];

        switch ($api) {
            case "h5":
                //返回支付跳转链接h5_url，为拉起微信支付收银台的中间页面，可通过访问该url来拉起微信客户端，完成支付，h5_url的有效期为5分钟。
                $url = "https://api.mch.weixin.qq.com/v3/pay/transactions/h5";
                break;
            case "native":
                //扫二维码支付
                //返回二维码链接code_url，此URL用于生成支付二维码，然后提供给用户扫码支付。注意：code_url并非固定值，使用时按照URL格式转成二维码即可。
                $url = "https://api.mch.weixin.qq.com/v3/pay/transactions/native";
                break;
            case "app":
                //返回预支付交易会话标识 prepay_id
                $url = "https://api.mch.weixin.qq.com/v3/pay/transactions/app";
                break;
            case "jsapi":
                //公众号、小程序支付
                //body参数必须要用户的openid
                //返回预支付交易会话标识 prepay_id
                $url = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
                $data["payer"] = ["openid" => $openid];
                break;
        }

        $data = jsonData($data);

        //签名值 通过HTTP Authorization头来传递请求签名
        $authorization = $this->authorization($url, "POST", time(), Formatter::nonce(), $data);

        //header
        $header   = self::CONFIG["header"];
        $header[] = $authorization;
        $header[] = "User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";

        //发送请求
        $res  = curlPost($url, $data, $header);
        $res  = json_decode($res,true);

        return $res;
    }

    /**
     * 查询订单
     *  商户可以通过查询订单接口主动查询订单状态，完成下一步的业务逻辑。查询订单状态可通过微信支付订单号或商户订单号两种方式查询
     * @param string $id
     * @param int $t
     * @return bool|mixed|string
     */
    public function orderQuery(string $id, int $t = 1)
    {
        switch ($t) {
            case 1://商户订单号查询
                $url = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{$id}?mchid=" . self::CONFIG["merchantId"];
                break;
            case 2://微信支付订单号查询
                $url = "https://api.mch.weixin.qq.com/v3/pay/transactions/id/{$id}?mchid=" . self::CONFIG["merchantId"];
                break;
        }
        return curlGet($url, [], self::CONFIG["header"]);
    }

    /**
     * 关闭订单
     * 以下情况需要调用关单接口：
     *   1、商户订单支付失败需要生成新单号重新发起支付，要对原订单号调用关单，避免重复支付；
     *   2、系统下单后，用户支付超时，系统退出不再受理，避免用户继续，请调用关单接口。
     * 注意：关单没有时间限制，建议在订单生成后间隔几分钟（最短5分钟）再调用关单接口，避免出现订单状态同步不及时导致关单失败。
     * @param string $out_trade_no
     * @return bool|mixed|string
     */
    public function orderClose(string $out_trade_no)
    {
        $url    = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{$out_trade_no}/close";
        $header = self::CONFIG["header"];
        $header["mchid"] = self::CONFIG["merchantId"];
        return curlPost($url, [], $header);
    }

    /**
     * 请求签名值
     * 通过HTTP Authorization头来传递请求签名
     */
    private function authorization($url, $http_method, $timestamp, $nonce, $body):string
    {
        $url_parts = parse_url($url);
        $canonical_url = ($url_parts["path"] . (!empty($url_parts["query"]) ? "?${url_parts["query"]}" : ""));

        $message = $http_method."\n".
            $canonical_url."\n".
            $timestamp."\n".
            $nonce."\n".
            $body."\n";

        $sign  = $this->signature($message);
        $token =  sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s',
                               self::CONFIG["merchantId"], $nonce, $timestamp, self::CONFIG["merchantApiCertSerialNo"], $sign);

        return "Authorization:WECHATPAY2-SHA256-RSA2048 {$token}";
    }

    /**
     * 使用商户私钥对待签名串进行SHA256 with RSA签名
     */
    private function signature(string $signStr):string
    {
        $privateKeyFilePath = "file://" . root_path() . self::CONFIG["payCert"] . "apiclient_key.pem";
        $privateKeyInstance = Rsa::from($privateKeyFilePath);
        return Rsa::sign($signStr, $privateKeyInstance);
    }


    /**
     * 支付回调验证处理
     */
    public function notifyVerify(array $header, string $inBody): bool
    {
        $inWechatpaySignature = $header["wechatpay-signature"];
        $inWechatpayTimestamp = $header["wechatpay-timestamp"];
        $inWechatpaySerial    = $header["wechatpay-serial"];
        $inWechatpayNonce     = $header["wechatpay-nonce"];

        // 根据通知的平台证书序列号，查询本地平台证书文件，
        $platformPublicKeyPath = root_path() . self::CONFIG["payCert"]. $inWechatpaySerial . ".pem";
        if (!file_exists($platformPublicKeyPath))
            $this->getCertificates();

        //验证证书
        $platformPublicKeyInstance = Rsa::from("file://".$platformPublicKeyPath, Rsa::KEY_TYPE_PUBLIC);
        $verifiedStatus = Rsa::verify(
            Formatter::joinedByLineFeed($inWechatpayTimestamp, $inWechatpayNonce, $inBody),// 构造验签名串
            $inWechatpaySignature,
            $platformPublicKeyInstance
        );
        // 检查通知时间偏移量，允许5分钟之内的偏移
        $timeOffsetStatus = 300 >= abs(Formatter::timestamp() - (int)$inWechatpayTimestamp);

        return $timeOffsetStatus && $verifiedStatus;
    }

    /**
     * 支付回调 加密文本消息解密
     */
    public function notifyDecryptText($ciphertext, $nonce, $aad): array
    {
        $inBodyResource = AesGcm::decrypt($ciphertext, self::CONFIG["apiV3Key"], $nonce, $aad);
        return (array)json_decode($inBodyResource, true);
    }

    /**
     * 获取平台证书列表
     */
    private function getCertificates()
    {
        $url = "https://api.mch.weixin.qq.com/v3/certificates";

        //签名值 通过HTTP Authorization头来传递请求签名
        $authorization = $this->authorization($url, "GET", time(), Formatter::nonce(), "");

        //header
        $header   = self::CONFIG["header"];
        $header[] = $authorization;
        $header[] = "User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";

        //发送请求
        $res = curlGet($url, [], $header);
        $res = json_decode($res,true);

        if (isset($res["data"])) {
            foreach ($res["data"] as $val) {
                $associatedData = $val["encrypt_certificate"]["associated_data"];
                $nonceStr       = $val["encrypt_certificate"]["nonce"];
                $ciphertext     = $val["encrypt_certificate"]["ciphertext"];
                $cert           = $this->decryptToString($associatedData, $nonceStr, $ciphertext);
                $path           = root_path() . self::CONFIG["payCert"]. $val["serial_no"] . ".pem";
                saveFile($path, $cert);
            }
        }
    }

    /**
     * 证书和回调报文解密
     * https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml
     *
     * @param string    $associatedData     AES GCM additional authentication data
     * @param string    $nonceStr           AES GCM nonce
     * @param string    $ciphertext         AES GCM cipher text
     *
     * @return string|bool      Decrypted string on success or FALSE on failure
     */
    const AUTH_TAG_LENGTH_BYTE = 16;
    private function decryptToString($associatedData, $nonceStr, $ciphertext)
    {
        $ciphertext = base64_decode($ciphertext);
        if (strlen($ciphertext) <= self::AUTH_TAG_LENGTH_BYTE) {
            return false;
        }

        // ext-sodium (default installed on >= PHP 7.2)
        if (function_exists("\sodium_crypto_aead_aes256gcm_is_available") && \sodium_crypto_aead_aes256gcm_is_available()) {
            return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, self::CONFIG["apiV3Key"]);
		}

        // ext-libsodium (need install libsodium-php 1.x via pecl)
        if (function_exists("\Sodium\crypto_aead_aes256gcm_is_available") && \Sodium\crypto_aead_aes256gcm_is_available()) {
            return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, self::CONFIG["apiV3Key"]);
		}

        // openssl (PHP >= 7.1 support AEAD)
        if (PHP_VERSION_ID >= 70100 && in_array("aes-256-gcm", \openssl_get_cipher_methods())) {
            $ctext = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
            $authTag = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);

            return \openssl_decrypt($ctext, "aes-256-gcm", self::CONFIG["apiV3Key"], \OPENSSL_RAW_DATA, $nonceStr,
				$authTag, $associatedData);
		}

        throw new \RuntimeException("AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php");
    }
}